


	/* Polyfills */

	if (!Function.prototype.bind) {
	  Function.prototype.bind = function (oThis) {
	    if (typeof this !== "function") {
	      // closest thing possible to the ECMAScript 5 internal IsCallable function
	      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
	    }

	    var aArgs = Array.prototype.slice.call(arguments, 1),
	        fToBind = this,
	        fNOP = function () {},
	        fBound = function () {
	          return fToBind.apply(this instanceof fNOP && oThis
	                                 ? this
	                                 : oThis,
	                               aArgs.concat(Array.prototype.slice.call(arguments)));
	        };

	    fNOP.prototype = this.prototype;
	    fBound.prototype = new fNOP();

	    return fBound;
	  };
	}

	(function(win, doc){
		if(win.addEventListener)return;		//No need to polyfill

		function docHijack(p){var old = doc[p];doc[p] = function(v){return addListen(old(v))}}
		function addEvent(on, fn, self){
			return (self = this).attachEvent('on' + on, function(e){
				var e = e || win.event;
				e.preventDefault  = e.preventDefault  || function(){e.returnValue = false}
				e.stopPropagation = e.stopPropagation || function(){e.cancelBubble = true}
				fn.call(self, e);
			});
		}
		function addListen(obj, i){
			if(!obj) return obj;
			if(i = obj.length)while(i--)obj[i].addEventListener = addEvent;
			else obj.addEventListener = addEvent;
			return obj;
		}

		addListen([doc, win]);
		if('Element' in win)win.Element.prototype.addEventListener = addEvent;			//IE8
		else{																			//IE < 8
			doc.attachEvent('onreadystatechange', function(){addListen(doc.all)});		//Make sure we also init at domReady
			docHijack('getElementsByTagName');
			docHijack('getElementById');
			docHijack('createElement');
			addListen(doc.all);
		}
	})(window, document);





	/* Utility functions */

	function loadWhichBrowser(cb) {
		var callback = cb;

		var p=[],w=window,d=document,e=f=0;p.push('ua='+encodeURIComponent(navigator.userAgent));e|=w.ActiveXObject?1:0;e|=w.opera?2:0;e|=w.chrome?4:0;
		e|='getBoxObjectFor' in d || 'mozInnerScreenX' in w?8:0;e|=('WebKitCSSMatrix' in w||'WebKitPoint' in w||'webkitStorageInfo' in w||'webkitURL' in w)?16:0;
		e|=(e&16&&({}.toString).toString().indexOf("\n")===-1)?32:0;p.push('e='+e);f|='sandbox' in d.createElement('iframe')?1:0;f|='WebSocket' in w?2:0;
		f|=w.Worker?4:0;f|=w.applicationCache?8:0;f|=w.history && history.pushState?16:0;f|=d.documentElement.webkitRequestFullScreen?32:0;f|='FileReader' in w?64:0;
		p.push('f='+f);p.push('r='+Math.random().toString(36).substring(7));p.push('w='+screen.width);p.push('h='+screen.height);
		
		var servers = [ 'api.whichbrowser.net', 'backup.whichbrowser.net' ];

		var timeout = null;

		function load() {
			if (typeof WhichBrowser != 'undefined') {
				return;
			}
			
			var server = servers.shift();
			if (server) {
				var script = document.createElement('script');
				script.src = '//' + server + '/rel/detect.js?' + p.join('&');
				document.getElementsByTagName('head')[0].appendChild(script);

				wait();
			}
		}

		function wait() {
			if (!timeout) {
				timeout = window.setTimeout(load, 3000);
			}

			if (typeof WhichBrowser == 'undefined') {
				window.setTimeout(wait, 100)
			}
			else {
				window.clearTimeout(timeout);
				callback();
			}
		}

		load();
	}

	function submit(method, payload) {
		var httpRequest;
		if (window.XMLHttpRequest) {
			httpRequest = new XMLHttpRequest();
		} else if (window.ActiveXObject) {
			httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
		}

		httpRequest.open('POST','/api/' + method, true);
		httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		httpRequest.send('payload=' + encodeURIComponent(payload));
	}

	function decodeParameters() {
		var params = {};
		
		if (location.search) {
			var parts = location.search.substring(1).split('&');

			for (var i = 0; i < parts.length; i++) {
				var nv = parts[i].split('=');
				if (!nv[0]) continue;
				params[nv[0]] = decodeURIComponent(nv[1]) || true;
			}
		}

		return params;
	}

	function upgradeConnection(success, failure) {
		if (location.protocol == "http:") {
			var httpRequest;

			if (window.XMLHttpRequest) {
				httpRequest = new XMLHttpRequest();
			} else if (window.ActiveXObject) {
				httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
			}
			
			httpRequest.onreadystatechange = function() {
				if (httpRequest.readyState == 4) {
					if (httpRequest.status >= 200 && httpRequest.status < 400) {
						success();
					} else {
						failure();
					}
				}
			}

			httpRequest.open('GET','https://' + location.hostname + '/assets/upgrade', true);
			httpRequest.send();

			return;
		}
		
		failure();
	}

	function submitResults(r, c) {
		var parameters = decodeParameters();
		var identifier = typeof parameters.identifier != 'undefined' ? parameters.identifier : '';
		var source = typeof parameters.source != 'undefined' ? parameters.source : '';
		var task = typeof parameters.task != 'undefined' ? parameters.task : '';

		try {
			var payload = '{' +
						'"release": "' + r.release + '",' +
						'"source": "' + source + '",' +
						'"protocol": "' + location.protocol + '",' +
						'"identifier": "' + escapeSlashes(identifier) + '",' +
						'"task": "' + task + '",' +
						'"uniqueid": "' + r.uniqueid + '",' +
						'"score": ' + c.score + ',' +
						'"maximum": ' + c.maximum + ',' +
						'"camouflage": "' + (Browsers.camouflage ? '1' : '0') + '",' +
						'"features": "' + (Browsers.features.join(',')) + '",' +
						'"browserName": "' + (Browsers.browser.name ? Browsers.browser.name : '') + '",' +
						'"browserChannel": "' + (Browsers.browser.channel ? Browsers.browser.channel : '') + '",' +
						'"browserVersion": "' + (Browsers.browser.version ? Browsers.browser.version.toString() : '') + '",' +
						'"browserVersionType": "' + (Browsers.browser.version ? Browsers.browser.version.type : '') + '",' +
						'"browserVersionMajor": "' + (Browsers.browser.version ? Browsers.browser.version.major : '') + '",' +
						'"browserVersionMinor": "' + (Browsers.browser.version ? Browsers.browser.version.minor : '') + '",' +
						'"browserVersionOriginal": "' + (Browsers.browser.version ? Browsers.browser.version.original : '') + '",' +
						'"browserMode": "' + (Browsers.browser.mode ? Browsers.browser.mode : '') + '",' +
						'"engineName": "' + (Browsers.engine.name ? Browsers.engine.name : '') + '",' +
						'"engineVersion": "' + (Browsers.engine.version ? Browsers.engine.version.toString() : '') + '",' +
						'"osName": "' + (Browsers.os.name ? Browsers.os.name : '') + '",' +
						'"osFamily": "' + (Browsers.os.family ? Browsers.os.family : '') + '",' +
						'"osVersion": "' + (Browsers.os.version ? Browsers.os.version.toString() : '') + '",' +
						'"deviceManufacturer": "' + (Browsers.device.manufacturer ? Browsers.device.manufacturer : '') + '",' +
						'"deviceModel": "' + (Browsers.device.model ? Browsers.device.model : '') + '",' +
						'"deviceSeries": "' + (Browsers.device.series ? Browsers.device.series : '') + '",' +
						'"deviceType": "' + (Browsers.device.type ? Browsers.device.type : '') + '",' +
						'"deviceIdentified": "' + (Browsers.device.identified ? '1' : '0' ) + '",' +
						'"deviceWidth": "' + (screen.width) + '",' +
						'"deviceHeight": "' + (screen.height) + '",' +
						'"useragent": "' + navigator.userAgent + '",' +
						'"humanReadable": "' + Browsers.toString() + '",' +
						'"points": "' + c.points + '",' +
						'"results": "' + r.results + '"' +
						'}';

			submit('submit', payload);
		} catch(e) {
			alert('Could not submit results: ' + e.message);
		}
	}

	function escapeSlashes(string) {
		return string.replace(/\\/g, '\\\\').
			replace(/\u0008/g, '\\b').
			replace(/\t/g, '\\t').
			replace(/\n/g, '\\n').
			replace(/\f/g, '\\f').
			replace(/\r/g, '\\r').
			replace(/'/g, '\\\'').
			replace(/"/g, '\\"');
	}


	var NO = 0,
		YES = 1,
		OLD = 2,
		BUGGY = 4,
		PREFIX = 8,
		BLOCKED = 16,
		DISABLED = 32,
		UNCONFIRMED = 64,
		UNKNOWN = 128,
		EXPERIMENTAL = 256;



	var Metadata = function() { this.initialize.apply(this, arguments) };
	Metadata.prototype = {
		initialize: function(data) {
			this.data = data;
			this.list = {};

			for (var i = 0; i < this.data.length; i++) {
				this.iterate(this.data[i].items, '', -1, []);
			}
		},

		iterate: function(data, prefix, level, path) {
			for (var i = 0; i < data.length; i++) {
				var key;

				if (typeof data[i].id != 'undefined') {
					key = prefix + (prefix == '' ? '' : '.') + data[i].id;
				}

				if (typeof data[i].key != 'undefined') {
					key = data[i].key;
				}

				if (key) {
					path[level + 1] = key;

					if (typeof data[i].key == 'undefined') {
						data[i].key = key;
					}

					if (typeof data[i].name != 'undefined') {
						this.list[key] = {
							name: data[i].name,
							path: path.slice(0, level + 1)
						};
					}

					if (typeof data[i].items != 'undefined') {
						this.iterate(data[i].items, key, level + 1, path);
					}
				}
			}
		},

		getItem: function(key) {
			var item = this.list[key];
			return item;
		},

		getTrail: function(key, separator) {
			var item = this.list[key];

			if (item) {
				var trail = [];

				for (var i = 0; i < item.path.length; i++) {
					if (typeof this.list[item.path[i]] != 'undefined') {
						trail.push(this.list[item.path[i]].name);
					} else {
						trail.push('?');
					}
				}

				trail.push(item.name);

				return trail.join(separator);
			}

			return '?';
		}
	}

	var Calculate = function() { this.initialize.apply(this, arguments) };
	Calculate.prototype = {
		initialize: function(test, data) {
			this.parameters = {
				test: 		test,
				data:		data
			};

			this.maximum = 0;
			this.score = 0;
			this.points = [];

			for (var i = 0; i < this.parameters.data.length; i++) {
				this.iterate(this.parameters.data[i].items, '');
			}

			this.points = this.points.join(',');
		},

		iterate: function(data, prefix) {
			for (var i = 0; i < data.length; i++) {
				if (data[i].key) {
					if (prefix == '') {
						var score = this.score;
						var maximum = this.maximum;
					}

					if (typeof data[i].value != 'undefined') {
						this.calculate(data[i].key, data[i]);
					}

					if (typeof data[i].items != 'undefined') {
						this.iterate(data[i].items, data[i].key);
					}

					if (prefix == '') {
						this.points.push(data[i].id + '=' + (this.score - score) + '/' + (this.maximum - maximum));
					}
				}
			}
		},

		calculate: function(prefix, data) {
			var result = true;
			var value = typeof data.value == 'object' ? data.value : { maximum: data.value };

			if (typeof data.value.conditional == 'undefined') {
				this.maximum += value.maximum;
			}

			if (typeof data.items == 'object') {
				result = 0;
				passed = true;

				for (var i = 0; i < data.items.length; i++) {
					if (data.items[i].key) {
						var r = this.getResult(data.items[i].key);
						passed &= r | YES;
						result |= r;
					}
				}

				if (!passed) {
					result = false;
				}
			}
			else {
				result = this.getResult(prefix);
			}

			if (result & YES) {
				var valid = true;

				if (typeof data.value.conditional == 'string') {
					if (data.value.conditional.substr(0, 1) == '!') {
						var conditional = this.getResult(data.value.conditional.substr(1));

						if (conditional & YES) {
							valid = false;
						}
					}
				}

				if (valid) {
					if (result & PREFIX && typeof value.award == 'object' && typeof value.award.PREFIX != 'undefined') {
						this.score += value.award.PREFIX;
					}
					else if (result & BUGGY && typeof value.award == 'object' && typeof value.award.BUGGY != 'undefined') {
						this.score += value.award.BUGGY;
					}
					else if (result & OLD && typeof value.award == 'object' && typeof value.award.OLD != 'undefined') {
						this.score += value.award.OLD;
					}
					else {
						this.score += value.maximum;
					}
				}
			}
		},

		getResult: function(key) {
			if (match = (new RegExp(key + '=(-?[0-9]+)')).exec(this.parameters.test.results)) {
				return parseInt(match[1], 10);
			}

			return null;
		}
	};



	/* Base UI functions */

	var Index = function() { this.initialize.apply(this, arguments) };
	Index.prototype = {
		initialize: function(options) {
			var that = this;
			this.options = options;

			var menu = document.createElement('div');
			menu.id = 'indexmenu';
			options.index.appendChild(menu);

			var categories = document.createElement('ul');
			menu.appendChild(categories);

			for (var i = 0; i < options.tests.length; i++) {
				var category = document.createElement('li');
				category.className = 'category ' + options.tests[i].id;
				categories.appendChild(category);

				var link = document.createElement('a');
				link.href = '#category-' + options.tests[i].id;
				link.onclick = function () { that.closeIndex(); };
				link.innerHTML = options.tests[i].name;
				category.appendChild(link);

				if (options.tests[i].items.length) {
					var items = document.createElement('ul');
					category.appendChild(items);

					for (var j = 0; j < options.tests[i].items.length; j++) {
						var item = document.createElement('li');
						items.appendChild(item);

						var link = document.createElement('a');
						link.href = '#table-' + options.tests[i].items[j].id;
						link.onclick = function () { that.closeIndex(); };
						link.innerHTML = options.tests[i].items[j].name;
						item.appendChild(link);
					}
				}
			}

			var button = document.createElement('button');
			button.innerHTML = '';
			button.id = 'indexbutton';
			button.onclick = this.toggleIndex;
			options.index.appendChild(button);

			options.wrapper.onclick = this.closeIndex;
		},

		toggleIndex: function() {
			if (document.body.className.indexOf(' indexVisible') == -1) {
				document.body.className = document.body.className.replace(' indexVisible', '') + ' indexVisible';
			} else {
				document.body.className = document.body.className.replace(' indexVisible', '');
			}
		},

		closeIndex: function() {
			document.body.className = document.body.className.replace(' indexVisible', '');
		}
	}


	var Confirm = function() { this.initialize.apply(this, arguments) };
	Confirm.prototype = {
		initialize: function(parent, options) {
			this.options = options;

			this.useragent = document.createElement('p');
			this.useragent.className = 'useragent';
			parent.appendChild(this.useragent);
			this.useragent.innerHTML = 'You are using ' + Browsers;

			this.confirm = document.createElement('span');
			this.confirm.innerHTML = 'Correct?';
			this.useragent.appendChild(this.confirm);

			var correct = document.createElement('a');
			correct.addEventListener('click', function() { this.confirmUseragent(); }.bind(this), true);
			correct.className = 'correct';
			correct.innerHTML = '✔';
			this.confirm.appendChild(correct);

			var wrong = document.createElement('a');
			wrong.addEventListener('click', function(e) { e.stopPropagation(); this.reportUseragent(); }.bind(this), true);
			wrong.className = 'wrong';
			wrong.innerHTML = '✘';
			this.confirm.appendChild(wrong);

			this.thanks = document.createElement('span');
			this.thanks.style.display = 'none';
			this.thanks.innerHTML = 'Thanks!';
			this.useragent.appendChild(this.thanks);
		},

		confirmUseragent: function() {
			this.options.onConfirm();
			this.showThanks();
		},

		reportUseragent: function() {
			this.options.onReport();

			new Feedback(this.confirm, {
				suggestion:	'I am using' + ' ' + Browsers,

				onFeedback:	function(value) {
								this.options.onFeedback(value);
							}.bind(this),

				onClose:	function() {
								this.showThanks();
							}.bind(this)
			});
		},

		showThanks: function() {
			this.confirm.style.display = 'none';
			this.thanks.style.display = 'inline';

			window.setTimeout(function() {
				this.thanks.style.display = 'none';
			}.bind(this), 2500);
		}
	}


	var Share = function() { this.initialize.apply(this, arguments) };
	Share.prototype = {
		initialize: function(parent, options) {
			var that = this;

			this.parent = parent;
			this.options = options;
			this.created = false;

			this.popup = document.createElement('div');
			this.popup.className = 'popupPanel pointsLeft share';
			this.popup.style.display = 'none';
			this.parent.appendChild(this.popup);

			this.parent.addEventListener('click', this.open.bind(this), true)
			this.parent.addEventListener('touchstart', this.open.bind(this), true)

			document.addEventListener('click', this.close.bind(this), true)
			document.addEventListener('touchstart', this.close.bind(this), true)
		},

		create: function() {
			this.created = true;

			this.popup.innerHTML +=
				"<div id='share'><div>" +

				"<div id='twitter'>" +
				"<a href='https://twitter.com/share' class='twitter-share-button' " +
					"data-url='https://html5test.com' " +
					"data-related='rakaz' " +
					"data-text='" + this.options.browser + " scored " + this.options.score + " points. How well does your browser support HTML5?' " +
					"data-lang='en' "+
					"data-count='vertical'"+
					">Tweet</a>" +
				"</div>" +

				"<div id='facebook'>" +
				"<iframe src='//www.facebook.com/plugins/like.php?href=https%3A%2F%2Fhtml5test.com&amp;width=60&amp;height=65&amp;colorscheme=light&amp;layout=box_count&amp;action=like&amp;show_faces=false&amp;send=false&amp;appId=202643099847776' scrolling='no' frameborder='0' style='border:none; overflow:hidden; width:The pixel width of the pluginpx; height:65px;' allowTransparency='true'></iframe>" +
				"</div>" +

				"<div id='google'>" +
				"<div class='g-plusone' " +
					"data-href='https://html5test.com' " +
					"data-size='tall'" +
					"></div>" +
				"</div>" +

				"</div></div>";



			!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id))
			{js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}
			(document,"script","twitter-wjs");

			(function() {
			var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
			po.src = 'https://apis.google.com/js/plusone.js';
			var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
			})();
		},

		open: function(e) {
			e.preventDefault();

			if (!this.created) {
				this.create();
			}

			this.popup.style.display = 'block';
		},

		close: function() {
			this.popup.style.display = 'none';
		}
	}


	var Save = function() { this.initialize.apply(this, arguments) };
	Save.prototype = {
		initialize: function(parent, options) {
			var that = this;

			this.parent = parent;
			this.options = options;
			this.created = false;

			this.popup = document.createElement('div');
			this.popup.className = 'popupPanel pointsLeft save';
			this.popup.style.display = 'none';
			this.parent.appendChild(this.popup);

			document.addEventListener('click', this.close.bind(this), false)
			document.addEventListener('touchstart', this.close.bind(this), false)

			this.parent.addEventListener('click', this.open.bind(this), true)
			this.parent.addEventListener('touchstart', this.open.bind(this), true)
		},

		create: function() {
			this.created = true;

			this.popup.innerHTML +=
				"<div id='save'>" +
				"<p>You can see the results here:</p>" +
				"<p><a href='" + document.location.protocol + "//html5te.st/" + this.options.id + "'>html5te.st/" + this.options.id + "</a></p>" +
				"<p>Or scan this QR-code:</p>" +
				"<p>" +
				"<img src='https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=" + encodeURIComponent(document.location.protocol + "//html5te.st/" + this.options.id) + "&choe=UTF-8' width='200' height='200'>" +
				"</p>" +
				"<p>The unique id for this test is:<br><code>" + this.options.id + "</code></p>" +
				"</div>";

			if (this.options.onSave) {
				this.options.onSave();
			}
		},

		open: function(e) {
			e.stopPropagation();

			var ignore = false;
			var el = e.target || e.srcElement;

			while (el != this.parent && ignore == false) {
				if (el.className.indexOf('popupPanel') != -1) ignore = true;
				el = el.parentNode;
			}

			if (!ignore) {
				e.preventDefault();

				if (!this.created) {
					this.create();
				}

				this.popup.style.display = 'block';
			}
		},

		close: function(e) {
			this.popup.style.display = 'none';
		}
	}



	var Feedback = function() { this.initialize.apply(this, arguments) };
	Feedback.prototype = {
		initialize: function(parent, options) {
			var that = this;

			this.parent = parent;
			this.options = options;

			this.popup = document.createElement('div');
			this.popup.className = 'popupPanel pointsRight feedback';
			this.popup.style.display = 'none';
			this.parent.appendChild(this.popup);

			this.popup.addEventListener('click', function(e) { e.stopPropagation() }, false)
			this.popup.addEventListener('touchstart', function(e) { e.stopPropagation() }, false)

			document.addEventListener('click', this.close.bind(this), false)
			document.addEventListener('touchstart', this.close.bind(this), false)

			this.create();
		},

		create: function() {
			this.created = true;

			this.popup.innerHTML +=
				"<div id='feedback'>" +
				"<h3>What is wrong with this identification?</h3>" +
				"<textarea id='correction'>" + this.options.suggestion + "</textarea>"+
				"<button id='sendCorrection'><span>✔</span>Send correction</button>" +
				"</div>";

			this.popup.style.display = 'block';

			var button = document.getElementById('sendCorrection')
			button.addEventListener('click', this.send.bind(this), false);
		},

		send: function() {
			var field = document.getElementById('correction')

			if (field.value != this.options.suggestion) {
				if (this.options.onFeedback) {
					this.options.onFeedback(field.value);
				}
			}

			this.close();
		},

		close: function(e) {
			this.popup.style.display = 'none';
			this.popup.parentNode.removeChild(this.popup);

			if (this.options.onClose) {
				this.options.onClose();
			}
		}
	}



	var ResultsTable = function() { this.initialize.apply(this, arguments) };
	ResultsTable.prototype = {

		initialize: function(options) {
			this.parent = options.parent;
			this.tests = options.tests;
			this.options = {
				title:			options.title || '',
				browsers:		options.browsers || [],
				columns:		options.columns || 2,
				distribute:		options.distribute || false,
				header:			options.header || false,
				links:			options.links || false,
				grading:		options.grading || false,
				features:		options.features || false,
				explainations:	options.explainations || false,

				onChange:		options.onChange || false
			}

			var that = this;

			function close(e) {
				if (that.panel) {
					var cell = that.panel.parentNode;
					var node = e.target;

					while (node.parentNode) {
						if (node == that.panel) return;
						node = node.parentNode;
					}

					that.panel.parentNode.removeChild(that.panel);
					that.panel = null;

					var node = e.target;
					while (node.parentNode) {
						if (node == cell) return e.stopPropagation();
						node = node.parentNode;
					}
				}
			}

			document.addEventListener('click', close, true)
			document.addEventListener('touchstart', close, true)

			this.data = [ null ];

			this.createCategories(this.parent, this.tests);
		},

		updateColumn: function(column, data) {
			this.data[column] = data;

			for (var c = 0; c < this.tests.length; c++)
			for (var i = 0; i < this.tests[c].items.length; i++) {
				var test = this.tests[c].items[i];

				if (typeof test != 'string') {
					if (typeof test != 'undefined') {
						var points = 0;
						var maximum = 0;

						if (match = (new RegExp(test.id + "=([0-9]+)(?:\\/([0-9]+))?(?:\\+([0-9]+))?")).exec(data.points)) {
							points = match[1];
							if (match[2]) maximum = match[2];
						}

						var row = document.getElementById('head-' + test.id);
						var cell = row.childNodes[0].firstChild.nextSibling;

						var content = "<div class='grade'>";

						if (this.options.grading) {
							var grade = '';
							var percent = parseInt(points / maximum * 100, 10);
							switch (true) {
								case percent == 0: 	grade = 'none'; break;
								case percent <= 30: grade = 'badly'; break;
								case percent <= 60: grade = 'reasonable'; break;
								case percent <= 95: grade = 'good'; break;
								default:			grade = 'great'; break;
							}

							if (points == maximum)
								content += "<span class='" + grade + "'>" + points + "</span>";
							else
								content += "<span class='" + grade + "'>" + points + "/" + maximum + "</span>";
						} else {
							content += "<span>" + points + "</span>";
						}

						content += "</div>";

						cell.innerHTML = content;
						this.updateItems(column, data, test.items);
					}
				}
			}
		},

		updateItems: function(column, data, tests) {
			var count = [ 0, 0 ];

			for (var i = 0; i < tests.length; i++) {
				if (typeof tests[i] != 'string') {
					var key = tests[i].key;

					var row = document.getElementById('row-' + key);
					var cell = row.childNodes[column + 1];

					if (typeof tests[i].items != 'undefined') {
						var results = this.updateItems(column, data, tests[i].items);

						if (results[0] == results[1])
							cell.innerHTML = '<div>Yes <span class="check">✔</span></div>';
						else if (results[1] == 0)
							cell.innerHTML = '<div>No <span class="ballot">✘</span></div>';
						else
							cell.innerHTML = '<div><span class="partially">Partial</span> <span class="partial">○</span></div>';
					}

					else {
						if (match = (new RegExp(key + '=(-?[0-9]+)')).exec(data.results)) {
							var result = parseInt(match[1], 10);

							if (result & YES) {
								switch(true) {
									case !! (result & BUGGY):		cell.innerHTML = '<div>Buggy <span class="buggy"></span></div>'; break;
									case !! (result & OLD):			cell.innerHTML = '<div>Partial <span class="partial">○</span></div>'; count[1]++; break;
									case !! (result & PREFIX):		cell.innerHTML = '<div>Prefixed <span class="check">✔</span></div>'; count[1]++; break;
									default:						cell.innerHTML = '<div>Yes <span class="check">✔</span></div>'; count[1]++; break;
								}
							}
							else {
								switch(true) {
									case !! (result & UNKNOWN):		cell.innerHTML = '<div>Unknown <span class="partial">?</span></div>'; break;
									case !! (result & BLOCKED):		cell.innerHTML = '<div>Not functional <span class="buggy">!</span></div>'; break;
									case !! (result & DISABLED):	cell.innerHTML = '<div>Disabled <span class="ballot">✘</span></div>'; break;
									default:						cell.innerHTML = '<div>No <span class="ballot">✘</span></div>'; break;
								}
							}
						} else {
							cell.innerHTML = '<div><span class="partially">Unknown</span> <span class="partial">?</span></div>';
						}
					}

					count[0]++;
				}
			}

			return count;
		},

		createCategories: function(parent, tests) {
			var left, right;

			left = document.createElement('div');
			left.className = 'left';
			left.innerHTML = '<div></div>';
			parent.appendChild(left);

			right = document.createElement('div');
			right.className = 'right';
			right.innerHTML = '<div></div>';
			parent.appendChild(right);


			for (var i = 0; i < tests.length; i++) {
				var container = parent;
				if (tests[i].column == 'left') container = left.firstChild;
				if (tests[i].column == 'right') container = right.firstChild;

				var div = document.createElement('div');
				div.className = 'category ' + tests[i].id;
				div.id = 'category-' + tests[i].id;
				container.appendChild(div);

				var h2 = document.createElement('h2');
				h2.innerHTML = tests[i].name;
				div.appendChild(h2);

				this.createSections(div, tests[i].items);
			}
		},

		createSections: function(parent, tests) {
			for (var i = 0; i < tests.length; i++) {
				var table = document.createElement('table');
				table.cellSpacing = 0;
				table.id = 'table-' + tests[i].id;
				parent.appendChild(table);

				var thead = document.createElement('thead');
				table.appendChild(thead);

				var tr = document.createElement('tr');
				tr.id = 'head-' + tests[i].id;
				thead.appendChild(tr);

				var th = document.createElement('th');
				th.innerHTML = tests[i].name + "<div></div>";
				th.colSpan = this.options.columns + 1;
				tr.appendChild(th);

				if (typeof tests[i].items != 'undefined') {
					var tbody = document.createElement('tbody');
					table.appendChild(tbody);

					var status = typeof tests[i].status != 'undefined' ? tests[i].status : '';

					this.createItems(tbody, 0, tests[i].items, {
						id:		tests[i].id,
						status:	status,
						urls:	[]
					});
				}
			}
		},

		createItems: function(parent, level, tests, data) {
			var ids = [];

			for (var i = 0; i < tests.length; i++) {
				var tr = document.createElement('tr');
				parent.appendChild(tr);

				if (typeof tests[i] == 'string') {
					if (this.options.explainations || tests[i].substr(0, 4) != '<em>') {
						var th = document.createElement('th');
						th.colSpan = this.options.columns + 1;
						th.className = 'details';
						tr.appendChild(th);

						th.innerHTML = tests[i];
					}
				} else {
					var key = tests[i].key;

					var th = document.createElement('th');
					th.innerHTML = "<div><span>" + tests[i].name + "</span></div>";
					tr.appendChild(th);

					for (var c = 0; c < this.options.columns; c++) {
						var td = document.createElement('td');
						tr.appendChild(td);
					}

					tr.id = 'row-' + key;

					if (level > 0) {
						tr.className = 'isChild';
					}

					if (typeof tests[i].items != 'undefined') {
						var urls = null;

						if (this.options.links) {
							if (typeof tests[i].urls != 'undefined') {
								urls = tests[i].urls;
							}
							else if (typeof tests[i].url != 'undefined') {
								urls = { 'w3c': tests[i].url };
							}
						}

						tr.className += 'hasChild';

						var children = this.createItems(parent, level + 1, tests[i].items, {
							id: 	key,
							status:	typeof tests[i].status != 'undefined' ? tests[i].status : data.status,
							urls:	urls
						});

						this.hideChildren(tr, children);

						(function(that, tr, th, children) {
							th.onclick = function() {
								that.toggleChildren(tr, children);
							};
						})(this, tr, th, children);
					} else {
						var urls;
						var value = 0;
						var showLinks = false;

						if (typeof tests[i].value != 'undefined') {
							value = tests[i].value;
						}

						if (typeof tests[i].urls != 'undefined') {
							urls = tests[i].urls;
							showLinks = true;
						}
						else if (typeof tests[i].url != 'undefined') {
							urls = [ [ 'w3c', tests[i].url ] ];
							showLinks = true;
						}

						th.className = 'hasLink';

						(function(that, th, data) {
							th.onclick = function() {
								new FeaturePopup(th, data);
							};
						})(this, th, {
							id:		key,
							name:	tests[i].name,
							value:	value,
							status:	typeof tests[i].status != 'undefined' ? tests[i].status : data.status,
							urls:	(urls || []).concat(data.urls || [])
						});
					}

					ids.push(tr.id);
				}
			}

			return ids;
		},

		toggleChildren: function(element, ids) {
			if (element.className.indexOf(' hidden') == -1) {
				this.hideChildren(element, ids);
			} else {
				this.showChildren(element, ids);
			}
		},

		showChildren: function(element, ids) {
			element.className = element.className.replace(' hidden', '');

			for (var i = 0; i < ids.length; i++) {
				var e = document.getElementById(ids[i]);
				e.style.display = 'table-row';
			}
		},

		hideChildren: function(element, ids) {
			element.className = element.className.replace(' hidden', '');
			element.className += ' hidden';

			for (var i = 0; i < ids.length; i++) {
				var e = document.getElementById(ids[i]);
				e.style.display = 'none';
			}
		}
	}



	var FeaturePopup = function() { this.initialize.apply(this, arguments) };
	FeaturePopup.current = null;
	FeaturePopup.prototype = {
		initialize: function(parent, data) {
			if (FeaturePopup.current) {
				FeaturePopup.current.close();
			}

			var maximum = data.value;
			if (typeof maximum == 'object') {
				maximum = maximum.maximum;
			}

			var content = "";
			content += "<div class='info'>";
			content += "<div class='column left status " + data.status + "'><span>" + data.status + "</span></div>";
			content += "<div class='column middle" + (maximum ? '' : ' none') + "'><em>" + ( maximum || '✘' ) + "</em> <span>" + (maximum != 1 ? 'Points' : 'Point') + "</span></div>";
			content += "<div class='column right'><a href='/compare/feature/" + data.id +".html' class='compare'><span>Compare</span></a></div>";
			content += "</div>";
			content += "<div class='links'>";

			for (var i = 0; i < data.urls.length; i++) {
				if (data.urls[i][0] == 'w3c') content += "<a href='" + data.urls[i][1] + "' class='w3c'>Go to the specification at W3C.org</a>";
				if (data.urls[i][0] == 'whatwg') content += "<a href='" + data.urls[i][1] + "' class='whatwg'>Go to the specification at Whatwg.org</a>";
				if (data.urls[i][0] == 'khronos') content += "<a href='" + data.urls[i][1] +"' class='khronos'>Go to the specification at Khronos.org</a>";
				if (data.urls[i][0] == 'ietf') content += "<a href='" + data.urls[i][1] +"' class='ietf'>Go to the specification at IETF.org</a>";
				if (data.urls[i][0] == 'webm') content += "<a href='" + data.urls[i][1] +"' class='webm'>Go to the specification at WebMproject.org</a>";
				if (data.urls[i][0] == 'xiph') content += "<a href='" + data.urls[i][1] +"' class='xiph'>Go to the specification at Xiph.org</a>";
				if (data.urls[i][0] == 'ecma') content += "<a href='" + data.urls[i][1] +"' class='ecma'>Go to the specification at ECMA</a>";
				if (data.urls[i][0] == 'ricg') content += "<a href='" + data.urls[i][1] +"' class='ricg'>Go to Responsive Images Community Group</a>";
				if (data.urls[i][0] == 'wp') content += "<a href='https://docs.webplatform.org/wiki" + data.urls[i][1] +"' class='wp'>Documentation at WebPlatform.org</a>";
				if (data.urls[i][0] == 'mdn') content += "<a href='https://developer.mozilla.org/en-US/docs" + data.urls[i][1] +"' class='mdn'>Documentation at Mozilla Developer Network</a>";
			}

			content += "</div>";

			this.panel = document.createElement('div');
			this.panel.className = 'linksPanel popupPanel pointsLeft';
			this.panel.innerHTML = content;
			parent.appendChild(this.panel);

			FeaturePopup.current = this;
		},

		close: function() {
			this.panel.parentNode.removeChild(this.panel);
			FeaturePopup.current = null;
		}
	}

	document.addEventListener('click', function() { if (FeaturePopup.current) FeaturePopup.current.close() }, true)
	document.addEventListener('touchstart', function() { if (FeaturePopup.current) FeaturePopup.current.close() }, true)
